package com.xyzwps.lib.express.router.ap;

import com.xyzwps.lib.ap.JavaFileWriter;
import com.xyzwps.lib.express.router.annotation.openapi.Operation;
import com.xyzwps.lib.express.router.annotation.openapi.Tags;
import com.xyzwps.lib.express.router.ap.util.CanonicalName;
import com.xyzwps.lib.openapi.element.Api;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import static com.xyzwps.lib.dollar.Dollar.*;

public class ApiAPOpenApiGen extends ApiAPGen {

    private final String[] tags;

    public ApiAPOpenApiGen(TypeElement typeElement) {
        super(typeElement);

        var tagsAnno = typeElement.getAnnotation(Tags.class);
        if (tagsAnno == null) {
            this.tags = new String[0];
        } else {
            var tags = tagsAnno.value();
            this.tags = Objects.requireNonNullElseGet(tags, () -> new String[0]);
        }
    }

    public void generate(ProcessingEnvironment processingEnv) {
        var apiClassName = CanonicalName.of(apiTypeElement.getQualifiedName().toString());
        var className = apiClassName.className() + "OpenApiCustomizer$AP";
        var packageName = apiClassName.packageName();

        var methods = collectApiMethods();
        var lines = new Lines();
        lines.add("package " + packageName + ";").add(null);

        lines.add("import com.xyzwps.lib.openapi.element.OpenApi;");
        lines.add("import com.xyzwps.lib.openapi.element.OpenApiCustomizer;");
        lines.add("import com.xyzwps.lib.openapi.element.Api;");
        lines.add("import com.xyzwps.lib.openapi.element.Schema;");
        lines.add("import com.xyzwps.lib.openapi.element.SchemaFormat;");
        lines.add("import jakarta.inject.Singleton;");
        lines.add(null);
        lines.add("import static com.xyzwps.lib.openapi.element.Schema.*;");
        lines.add(null);

        lines.add("@Singleton");
        lines.add("public class " + className + " implements OpenApiCustomizer {").indentPP().add(null);


        lines.add("@Override");
        lines.add("public void accept(OpenApi doc) {").indentPP().add(null);

        var schemaToSourceCode = new OpenApiSchemaToSourceCodeVisitor(lines);

        int i = 1;
        for (var method : methods) {
            var apiInfo = ApiInfo.from(method);
            var operationInfo = new OperationInfo(method, this.tags);
            lines.add("// " + i++);
            lines.add("{").indentPP();

            lines.add("var api = new Api(\"" + apiInfo.httpMethod().toLowerCase() + "\", \"" + apiPrefix + apiInfo.path() + "\");");

            for (var arg : apiInfo.arguments()) {
                switch (arg) {
                    case ApiMethodArgument.RequestParam it -> {
                        switch (it.in()) {
                            case PATH ->
                                    lines.add("var p = api.addPathParam(\"" + it.name() + "\", " + it.type() + ".class);");
                            case QUERY ->
                                    lines.add("var p = api.addSearchParam(\"" + it.name() + "\", " + it.type() + ".class);");
                            case HEADER ->
                                    lines.add("var p = api.addHeaderParam(\"" + it.name() + "\", " + it.type() + ".class);");
                            case COOKIE ->
                                    throw new IllegalStateException("Unsupported cookie params"); // TODO: cookie params
                        }

                        if ($.isNotEmpty(it.description())) {
                            lines.add("p.description(\"" + $.unescape(it.description()) + "\");");
                        }
                        var schema = it.schema();
                        if (schema == null) {
                            lines.add("p.schema(null);");
                        } else {
                            schema.accept(schemaToSourceCode, "schema");
                            lines.add("p.schema(schema);");
                        }
                    }
                    case ApiMethodArgument.Body it -> lines.add("api.addRequestBody(" + it.type() + ".class);");
                    default -> System.out.println();
                }
            }

            var tags = operationInfo.tags();
            for (var tag : tags) {
                lines.add("api.addTag(\"" + $.unescape(tag) + "\");");
            }

            if ($.isNotBlank(operationInfo.summary())) {
                lines.add("api.addSummary(\"" + $.unescape(operationInfo.summary()) + "\");");
            }
            if ($.isNotBlank(operationInfo.description())) {
                lines.add("api.addDescription(\"" + $.unescape(operationInfo.description()) + "\");");
            }
            if ($.isNotBlank(operationInfo.operationId())) {
                lines.add("api.setOperationId(\"" + operationInfo.operationId() + "\");");
            } else {
                lines.add("api.setOperationId(\"" + Api.operationId(apiInfo.httpMethod(), apiPrefix + apiInfo.path()) + "\");");
            }

            lines.add("api.setDeprecated(" + operationInfo.deprecated + ");");

            generateResponseSchema(lines, apiInfo.returnType());
            lines.add("api.addResponseSchema(resp);");
            lines.add("doc.addApi(api);");
            lines.indentMM().add("}")
                    .add("");
        }

        lines.indentMM().add("} // end add").add(null);

        lines.indentMM().add("} // end class");

        JavaFileWriter.write(packageName + "." + className, lines.toString(), processingEnv);
    }

    private static final class OperationInfo {
        private final List<String> tags = new ArrayList<>();
        private String summary = null;
        private String description = null;
        private String operationId = null;
        private boolean deprecated = false;

        OperationInfo(ExecutableElement method, String[] tags) {
            if (tags != null) {
                this.tags.addAll(Arrays.asList(tags));
            }

            var tagsAnno = method.getAnnotation(Tags.class);
            if (tagsAnno != null && tagsAnno.value() != null) {
                this.tags.addAll(Arrays.asList(tagsAnno.value()));
            }

            var opAnno = method.getAnnotation(Operation.class);
            if (opAnno != null) {
                this.summary = opAnno.summary();
                this.description = opAnno.description();
                this.operationId = opAnno.operationId();
                this.deprecated = opAnno.deprecated();

                if (opAnno.tags() != null) {
                    this.tags.addAll(Arrays.asList(opAnno.tags()));
                }
            }
        }

        public List<String> tags() {
            return this.tags;
        }

        public String summary() {
            return this.summary;
        }

        public String description() {
            return this.description;
        }

        public String operationId() {
            return this.operationId;
        }

        public boolean deprecated() {
            return this.deprecated;
        }
    }


    private static void generateResponseSchema(Lines lines, TypeMirror returnType) {
        switch (returnType) {
            case NoType nt -> {
                if (Objects.requireNonNull(nt.getKind()) == TypeKind.VOID) {
                    lines.add("Schema resp = null;");
                } else {
                    throw new IllegalStateException("unsupported return type: " + returnType);
                }
            }
            case PrimitiveType pt -> {
                switch (pt.getKind()) {
                    case BOOLEAN -> lines.add("var resp = new BooleanSchema();");
                    case SHORT, INT, LONG -> lines.add("var resp = new IntegerSchema();");
                    case FLOAT, DOUBLE -> {
                    }
                    case CHAR -> lines.add("var resp = new StringSchema();");
                    default -> throw new IllegalStateException("unsupported return type: " + returnType);
                }
            }
            case ArrayType at -> {
                lines.add("var resp = new ObjectSchema();"); // TODO:
            }
            case DeclaredType dt -> {
                lines.add("var resp = new ObjectSchema();"); // TODO:
                System.out.println(dt);
            }
            default -> throw new IllegalStateException("unsupported return type: " + returnType);
        }
    }
}
